2. Cpp面向对象编程(OOP)入门
C++代码块没有包含头文件时,默认是:
#include <iostream>
using namespace std;
1 类和继承
1.1 类的概念
1.1.1 结构体(Struct, C)
用 . 访问结构体的成员.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 借助结构体进行数据抽象
struct Student{
int ID;
int classNum;
string name;
};
int main() {
vector<Student> students; // 声明一个向量students, 元素类型是结构体Student
Student stu1; // 声明结构体对象stu1
stu1.ID = 5; // 用点号访问结构体中的成员
stu1.classNum = 1;
stu1.name = "大宝";
students.push_back(stu1); // 将stu1添加到students数组
Student stu2; // 类似上面
stu2.ID = 6;
stu2.classNum = 1;
stu2.name = "小宝";
students.push_back(stu2);
for ( int i = 0; i < 2; i++ ) {
cout << "该学生的学号是:" << students[i].ID << endl;
cout << "该学生的班级是:" << students[i].classNum << endl;
cout << "该学生的名字是:" << students[i].name << endl;
cout << endl;
}
return 0;
}
-----------------------------------------------------------
输出:
该学生的学号是:5
该学生的班级是:1
该学生的名字是:大宝
该学生的学号是:6
该学生的班级是:1
该学生的名字是:小宝
1.1.2 封装
封装: 隐藏细节, 将一个复杂的装置封装在一个黑匣子, 只提供一些用户接口. 如, 在使用函数时, 只需要关注参数类型 && 返回值 && 函数的作用, 而不需要关系函数体内具体的代码.
类(Class)是 C++在 C 的结构体之上进行扩展的新类型, 可以同 Struct 一般实现数据抽象, 也可以使用访问控制符有效地体现封装. 比如, 上面的 vector.push_back(...);, 就是 vector 类的使用, 我们可以使用它的 push_back() 和 pop_back() 等函数, 而不必关心 vector 是如何实现的.
1.1.3 继承和多态
在设计 Class 的时候, 为了避免重复的数据和函数, 就会使用继承来让类形成一个树形的层级结构.
继承也是为了实现多态而存在的. 多态: 不同的类具有相似的行为. 在 C++中可以使用虚函数声明同名的函数, 并在不同的类中进行不同的实现.
1.2 类的定义
面向对象的核心是类, 它是 C++ 在 C 语言原有结构体的基础之上扩展出来的概念, 不仅增加了附属于类的成员函数, 也增加了继承和虚函数等面向对象编程所需要的重要功能.
从 Class 创建出来的具体变量(实例)则叫作对象(Object). 每个对象占有着独立的内存空间, 而类只是一个描述对象的抽象概念(模板). 对象和类可以理解为是糕点和做糕点的模具, 糕点的形状都相同, 但可能是不同材料做的. 当然, 有时我们也会将这两个术语混用. 先来看一个简单的类定义.
#include <iostream>
#include <string>
using namespace std;
// 类的定义
class Champion{
public:
Champion(int id, string nm, int hp, int mn, int dmg) { // 此为构造函数, 见后文
ID = id;
name = nm;
HP = hp;
mana = mn;
damage = dmg;
}
void attack(Champion &chmp) { // 攻击. 当当前英雄(调用`attack`函数的英雄)发起攻击时, 会调用被攻击英雄(`chmp`)的`takeDamage`函数, 并传入当前英雄的`damage`(伤害值). 这里的`this->damage`指的是 “当前攻击者” 的伤害属性(`this`指针指向当前调用`attack`函数的英雄对象).
chmp.takeDamage(this->damage); // 此处this指针, 表示当前攻击者的damage而不是被攻击者的damage, 实测此处this指针并不是必须的
}
void takeDamage(int incomingDmg) { // 掉血. 当英雄受到伤害时, 将自身的`HP`(血量)减去受到的伤害值(`incomingDmg`), 直接修改自身的血量属性.
HP -= incomingDmg;
}
private:
int ID;
string name;
int HP; //血量
int mana; //魔法值
int damage; //伤害值
};
int main() {
return 0;
}
1.2.1 成员变量
类中可以定义各种成员变量, 可以是基本数据类型或其他类的实例, 其初始值需要在构造函数中指定.
一个类的内部可再定义一个类, 不过该类的作用域将仅限于外层类中. 若是公有访问(public)级别, 则可使用作用域操作符 :: 在外部访问. 私有成员(private)一般仅限于类内部定义的成员函数的访问(封装).
1.2.2 Member Function (成员函数)
调用某个类实例的成员函数时, 使用 myObj.doSomething() 语法.
成员函数可以在类之内/外部定义. 若要在外部定义, 需要首先在类中声明函数, 然后在类外定义的函数名前加上类名和作用域符 ::.
#include <iostream>
#include <string>
using namespace std;
// 成员函数定义与声明的分离
class Champion{
public:
Champion(int id, string nm, int hp, int mn, int dmg); // 类内部提前声明
void attack(Champion &); // 类似的提前声明, 函数声明不需要像上一个代码块指定形参名`chmp`
void takeDamage(int incomingDmg); // 类似的提前声明
private: // 私有成员
int ID;
string name;
int HP; //血量
int mana; //魔法值
int damage; //伤害值
};
Champion::Champion(int id, string nm, int hp, int mn, int dmg) { // 使用类名+作用域符 `::`在类之外定义成员函数(此处是构造函数)
ID = id;
name = nm;
HP = hp;
mana = mn;
damage = dmg;
}
void Champion::attack(Champion &chmp) { // 类似的定义方式
chmp.takeDamage(this->damage);
}
void Champion::takeDamage(int incomingDmg) { // 类似的定义方式
HP -= incomingDmg;
}
int main() {
return 0;
}
常量成员函数
在参数列表(函数声明或定义时)后加上 const 关键字(如 Champion(int id, string nm, int hp, int mn, int dmg) const; ), 可以创建常量成员函数, 在该函数中不可修改本对象的成员变量.
1.2.3 构造函数
在上述代码中, 有一个和类同名的特殊成员函数 Champion(), 称为 Constructor (构造函数). 其主要功能是初始化成员变量, 往往在类实例化(创建对象)时自动调用.
构造函数支持重载. 没有参数的构造函数被称为默认构造函数(Default Constructor). 没有自定义构造函数时, 系统会提供一个预置的默认构造函数, 将所有成员变量都初始化为默认值(与具体数据类型有关).
1.2.4 对象创建和使用
使用成员访问运算符 . 访问类实例的成员(函数和变量).
#include <iostream>
#include <string>
using namespace std;
class Champion{
public:
Champion(int id, string nm, int hp, int mn, int dmg) {
ID = id;
name = nm;
HP = hp;
mana = mn;
damage = dmg;
}
void attack(Champion &chmp) {
chmp.takeDamage(this->damage);
}
void takeDamage(int incomingDmg) {
HP -= incomingDmg;
}
int getHP() {
return HP;
}
private: // 私有成员不能在类实例访问, 如galen.ID不可行. 只能在类定义中被成员函数等访问
int ID;
string name;
int HP; //血量
int mana; //魔法值
int damage; //伤害值
};
int main() {
Champion galen(1, "Galen", 800, 100, 10); // 定义对向时, 类名Champion被当作类型名使用, 后面括号是构造函数的参数. 若不给出参数, 将会调用自定义或系统预设的默认构造函数
Champion ash(2, "Ash", 700, 150, 7);
cout << "Ash的初始血量:" << ash.getHP() << endl;
galen.attack(ash); // 成员访问运算符 "."
cout << "Ash受到Galen攻击后的血量:" << ash.getHP() << endl;
return 0;
}
---------------------------------------------------------------------------
输出:
Ash的初始血量:700
Ash受到Galen攻击后的血量:690
也可以使用类的指针访问成员(函数或变量):
#include <iostream>
#include <string>
using namespace std;
class Champion{
public:
Champion(int id, string nm, int hp, int mn, int dmg) {
ID = id;
name = nm;
HP = hp;
mana = mn;
damage = dmg;
}
void attack(Champion &chmp) {
chmp.takeDamage(this->damage);
}
void takeDamage(int incomingDmg) {
HP -= incomingDmg;
}
int getHP() {
return HP;
}
private:
int ID;
string name;
int HP; //血量
int mana; //魔法值
int damage; //伤害值
};
int main() {
Champion galen(1, "Galen", 800, 100, 10);
Champion ash(2, "Ash", 700, 150, 7);
cout << "Ash的初始血量:" << ash.getHP() << endl;
Champion *chmpPtr = &galen; // 指向Champion的指针
(*chmpPtr).attack(ash); // 可以解引用再使用点符号
chmpPtr->attack(ash); // 也可以使用指针专用的成员访问符 "->", 两者等价.
cout << "Ash受到Galen攻击后的血量:" << ash.getHP() << endl;
return 0;
}
----------------------------------------------------------------
输出:
Ash的初始血量:700
Ash受到Galen攻击后的血量:680
也可以直接在类定义之后紧跟对象声明:
#include <iostream>
using namespace std;
class MyClass{
public:
MyClass() {
a = 1;
}
int getA() {
return a;
}
private:
int a;
}myclass; // 在类MyClass定义之后紧跟对象myclass声明
int main() {
cout << "a的值是:" << myclass.getA() << endl;
return 0;
}
-----------------------------------------------------
输出:
a的值是:1
1.2.5 this 指针
在成员函数访问成员时, 有一个隐含的指针变量 this 可用, 它的类型是指向当前实例的指针, 即指向正在调用该成员函数的对象.
#include <iostream>
using namespace std;
// this指针的显式使用
class MyClass{
public:
MyClass(int a, int b) {
this->a = a; // this指针就是当前类实例的地址/指针, 相当于python的`self`
this->b = b;
}
int getA() {
return this->a; // 等价于 `return a;`
}
private:
int a;
int b;
};
int main() {
MyClass myclass(2, 3);
cout << "a的值是:" << myclass.getA() << endl;
return 0;
}
-----------------------
输出:
a的值是:2
this 指针也可以作为成员函数的返回值, 用于获取对象的地址或副本:
#include <iostream>
using namespace std;
class MyClass{
public:
MyClass(int a, int b) {
this->a = a;
this->b = b;
}
MyClass *getAddr() {
return this; // 获取地址, 返回 MyClass*
}
MyClass getCopy() {
return *this; // 获取副本, 返回 MyClass
}
int getA() { return a; }
private:
int a;
int b;
};
int main() {
MyClass myclass(2, 3);
MyClass *ptr = myclass.getAddr(); // 注意其用法
cout << "a的值是:" << ptr->getA() << endl;
MyClass copy = myclass.getCopy();
cout << "a的值是:" << copy.getA() << endl;
return 0;
}
-----------------------------------------------------------------------------
输出:
a的值是:2
a的值是:2
1.2.6 类和结构体的区别
结构体的默认访问控制符是 public, 而类是 private:
#include <iostream>
using namespace std;
// struct和class的默认访问控制符
class MyClass{
// 加上public程序可以运行, 否则默认为private成员
//public:
MyClass(int a, int b) {
this->a = a;
this->b = b;
}
int a;
int b;
};
struct MyStruct {
// struct默认是public
MyStruct(int a, int b) {
this->a = a;
this->b = b;
}
int a;
int b;
};
int main() {
MyClass myclass(2, 3); // error: ‘MyClass::MyClass(int, int)’ is private within this context, 私有成员函数无法访问
MyStruct mystruct(2, 3);
cout << "a的值是:" << myclass.a << endl; // error: ‘int MyClass::a’ is private within this context, 私有成员变量无法访问
cout << "a的值是:" << mystruct.a << endl;
return 0;
}
C 中的结构体没有构造函数和成员函数这些面向对象的元素, 而 C++ 补全了这一点. C++当中的 struct 也可以使用关键字 public 和 private, 因此两者的区别仅有上述一点: 默认访问控制符不同.
1.3 Constructor (构造函数)
1.3.1 默认构造函数
一般而言, 构造函数都会接受参数来初始化类的成员, 而默认构造函数是无参数的. 在创建对象时, 若对象名后面不加括号, 系统就会自动调用默认构造函数:
#include <iostream>
using namespace std;
class Time{
public:
Time() { // 默认构造函数不接受参数
hour = 0;
minute = 0;
second = 0;
}
int hour;
int minute;
int second;
};
int main() {
Time time; // 创建对象时, 若对象名后面不加括号, 系统就会自动调用默认构造函数.
cout << "时间是:" << time.hour << "时" << time.minute << "分" << time.second << "秒" << endl;
return 0;
}
-----------------------------------------------
输出:
时间是:0时0分0秒
如果构造函数完全不存在, 系统就会自动生成一个默认构造函数, 它遵循基本数据类型的默认初始化规则.
1.3.2 重载构造函数
#include <iostream>
using namespace std;
class Area{
public:
Area(int a, int b) {
area = a * b;
}
Area(int a) { // 重载构造函数
area = a * a;
}
int getArea() { return area; }
private:
int area;
};
int main() {
int a = 3;
int b = 4;
int c = 5;
Area area1(a, b);
Area area2(c);
cout << "边长为" << a << "和" << b << "的长方形面积为:" << area1.getArea() << endl;
cout << "边长为" << c << "的正方形面积为:" << area2.getArea() << endl;
return 0;
}
------------------------------------------------------
输出:
边长为3和4的长方形面积为:12
边长为5的正方形面积为:25
1.3.3 初始化列表
除了使用赋值对类成员进行初始化, 也可以使用初始化列表来完成这一操作:
#include <iostream>
using namespace std;
class Time{
public:
Time(int hr, int min, int sec) : hour(hr), minute(min), second(sec) {} // 初始化列表以一个":"开始, 位于参数列表和函数体之间, 等同于赋值 hour=hr, minute=min, second=sec.
int getHour() { return hour; }
int getMinute() { return minute; }
int getSecond() { return second; }
private:
int hour;
int minute;
int second;
};
int main() {
Time time(12, 24, 36);
cout << "时间是:" << time.getHour() << "时"
<< time.getMinute() << "分"
<< time.getSecond() << "秒" << endl;
return 0;
}
--------------------------------------------------------
输出:
时间是:12时24分36秒
实际上, 基本数据类型也可以用类似的语法进行初始化:
int main() {
int num(5);
float fnum(3.4);
char ch('h');
bool bval(false);
return 0;
}
初始化列表和赋值两种方式的区别可见下两个代码:
- 赋值
#include <iostream>
using namespace std;
// 初始化列表的调用顺序
// Author: 零壹快学
class B{
public:
B() {
cout << "B的构造函数被调用!" << endl;
num = 0;
}
private:
int num;
};
class A{
public:
A(B bb) {
cout << "A的构造函数被调用!" << endl;
num = 0;
b = bb;
}
private:
B b;
int num;
};
int main() {
B b;
cout << "创建A的对象之前!" << endl;
A a(b);
return 0;
}
-----------------------------------------------
输出:
B的构造函数被调用!
创建A的对象之前!
B的构造函数被调用!
A的构造函数被调用!
A 的成员 b 会先调用 B 的默认构造函数(因此出现输出的第三行), 再在 A 的构造函数体内通过 b=bb 赋值(一次赋值操作).
- 初始化列表
```cpp
#include <iostream>
using namespace std;
class B {
public:
B(int x) { // 给B添加带参数的构造函数, 方便观察
cout << "B的带参构造被调用! x=" << x << endl;
}
B() { // 保留默认构造
cout << "B的默认构造被调用!" << endl;
}
};
class A {
public:
// 使用初始化列表, 故意打乱顺序: 先初始化num,再初始化b. 依然会先构造`b`, 再初始化`num`(因为`b`声明在前). 注: `int`是基本类型, 初始化顺序不影响结果, 但对于对象类型, 这个顺序至关重要
A(B bb) : num(10), b(bb) { // 初始化列表:num在前,b在后
cout << "A的构造函数体执行!" << endl;
}
private:
// 成员声明顺序: 先声明b, 再声明num
B b;
int num;
};
int main() {
B b(20); // 创建B对象, 调用B的带参构造
cout << "创建A对象前..." << endl;
A a(b); // 创建A对象, 传入参数b
return 0;
}
-----------------------------------------------------
输出:
B的带参构造被调用! x=20
创建A对象前...
A的构造函数体执行!
A 的成员 b 的初始化会直接调用 B 的拷贝构造函数(编译器自动生成), 用 bb 直接初始化 b. 这个过程只需要一次拷贝构造, 省去了默认构造 + 赋值(以及 B 的默认构造函数中其他可能的语句)的额外开销, 因此更高效.
必须使用初始化列表的情况包括: 没有默认构造函数的成员, 引用成员, 常量成员. 因为引用成员和常量成员建议在声明的同时进行初始化.
#include <iostream>
using namespace std;
// 必须使用初始化列表的情况
class B{
public:
private:
int num;
};
class A{
public:
A(int num) : b(), numRef(num), num(num) { // 初始化列表
}
private:
B b; // 没有默认构造函数的成员
int &numRef; // 引用成员
const int num; // 常量成员
};
int main() {
A a(2);
return 0;
}
1.4 Destructor (析构函数)
既然有用来初始化类的构造函数, 也有一种用来释放内存/收尾的析构函数.
1.4.1 语法
析构函数和构造函数一样使用类名作为函数名, 但是析构函数函数名之前多一个 ~. 下面示例中, 类实例 myclass 在 main() 中创建, 被分配在栈上, main() 结束时会被自动销毁, 此时自动调用析构函数.
#include <iostream>
using namespace std;
class MyClass{
public:
MyClass(int a, int b) : a(a), b(b) { cout << "构造函数被调用!" << endl; }
~MyClass() { cout << "析构函数被调用!" << endl; } // 析构函数
void printAB() {
cout << "a的值是:" << a << ", b的值是:" << b << endl;
}
private:
int a;
int b;
};
int main() {
MyClass myclass(2, 3);
myclass.printAB();
return 0;
}
-----------------------------------------------------
输出:
构造函数被调用!
a的值是:2, b的值是:3
析构函数被调用! // `main()` 结束时myclass会被自动销毁, 此时自动调用析构函数.
当然, 析构函数也可以显式调用(高级内容, 待补充). #todo
1.4.2 动态分配对象内存
前面提到 new 和 delete 可以管理动态内存分配. 对于类的实例, 使用 new 会自动调用构造函数, delete 会自动调用析构函数.
#include <iostream>
using namespace std;
class MyClass{
public:
MyClass(int a, int b) : a(a), b(b) { cout << "构造函数被调用!" << endl; }
~MyClass() { cout << "析构函数被调用!" << endl; }
void printAB() {
cout << "a的值是:" << a << ", b的值是:" << b << endl;
}
private:
int a;
int b;
};
int main() {
MyClass *ptr = new MyClass(2, 3); // 调用构造函数
ptr->printAB(); // 调用成员函数
delete ptr; // 调用析构函数
ptr = new MyClass(6, 5); // 调用构造函数, 注意, new和delete返回的都是指针
ptr->printAB(); // 调用成员函数
delete ptr; // 调用析构函数
return 0;
}
------------------------------------------------
输出:
构造函数被调用!
a的值是:2, b的值是:3
析构函数被调用!
构造函数被调用!
a的值是:6, b的值是:5
析构函数被调用!
也可以在类的内部实现成员变量的动态内部分配, 注意, 在构造函数用 new 分配的成员变量, 需要在析构函数用 delete 释放.
#include <iostream>
using namespace std;
class MyClass{
public:
MyClass(int size) : size(size) {
arr = new int[size]; // 创建
for ( int i = 0; i < size; i++ ) {
arr[i] = i;
}
}
~MyClass() { delete[] arr; } // 释放
void printArr() {
for ( int i = 0; i < size; i++ ) {
cout << arr[i] << " ";
}
cout << endl;
}
private:
int *arr;
int size;
};
int main() {
MyClass obj(5);
obj.printArr();
return 0;
}
---------------------------------------------------------
输出:
0 1 2 3 4
1.5 类的作用域
1.5.1 作用域操作符
在类的定义体之外定义成员函数和成员变量需要使用作用域操作符 ::, 表示该成员属于此类, 以避免在两个类分别定义同名成员时发生问题.
#include <iostream>
using namespace std;
class A{
public:
A() {}
static int num;
private:
};
int A::num = 2; // 使用作用域操作符
int num = 3; // 普通的全局变量
int main() {
cout << "A::num的值为:" << A::num << endl;
cout << "num的值为:" << num << endl;
return 0;
}
---------------------------------------------------------------------------------
输出:
A::num的值为:2
num的值为:3
除了对类实例使用成员访问运算符 ., 也可以在一个类A中, 用作用域操作符 :: 使用另一个类B的成员函数:
#include <iostream>
using namespace std;
class B{
public:
B() { }
static void printB() { cout << "print B!" << endl; }
};
class A{
public:
A() {}
void printB() { B::printB(); } // 用作用域操作符 `::` 使用另一个类B的成员函数
private:
int num;
};
int main() {
A a;
a.printB();
return 0;
}
---------------------------------------------------------------------------------
输出:
print B!
1.5.2 名字查找
在使用变量/调用函数时, 编译器需要确认当前变量名/函数名对应的声明是哪一个, 因为复合语句&&函数&&类等等会引入复杂的作用域. 所以如果没处理好声明的顺序/大量使用相同的名字, 可能会使得程序陷入找不到声明&&重定义的问题, 因此有必要了解名字查找的规则.
- 一般的, 没有类的情形: 从内向外查找. 如果有几个嵌套作用域(不论是函数还是复合语句), 编译器在遇到第一个名字时, 就会从当前语句开始往前查找声明, 如果查找不到, 再一层层向外向前搜索.
#include <iostream>
using namespace std;
int main() {
bool cond = true;
int a = 2;
if ( cond ) {
cout << "a的值是:" << a << endl; // 输出为2
int a = 3;
}
return 0;
}
- 有类定义时:
- 声明部分的查找和前面的一般情况一致;
- 成员函数定义的查找规则: 先函数作用域, 再类定义, 再外层作用域.
class A{
public:
A() { num = 0; } // num定义在后面, 但不会报错. 因为类定义在编译时, 会先编译声明部分, 也就是先跳过此行函数体. 编译完声明后, 才会编译定义, 因为一些成员函数的定义会放在类定义外面.
NUM getNum() { return num; } // 和普通情况一样, 编译器找不到NUM, 因为类型名NUM的声明放在了这行后面
typedef int NUM;
private:
int num;
};
int main() {
A a;
a.getNum();
return 0;
}
------------------------------------------------------------------------------
/home/siecho/Desktop/zj_obsidian/C++/C++ code/8.5.4.cpp:6:17: error: ‘NUM’ does not name a type
6 | NUM getNum() { return num; }
| ^~~
/home/siecho/Desktop/zj_obsidian/C++/C++ code/8.5.4.cpp: In function ‘int main()’:
/home/siecho/Desktop/zj_obsidian/C++/C++ code/8.5.4.cpp:14:11: error: ‘class A’ has no member named ‘getNum’
14 | a.getNum();
| ^~~~~~
make[2]: *** [CMakeFiles/case.dir/build.make:76: CMakeFiles/case.dir/C++_code/8.5.4.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:83: CMakeFiles/case.dir/all] Error 2
make: *** [Makefile:91: all] Error 2
正如一般的作用域内外的变量相互屏蔽, 类中也可能出现屏蔽成员的情况.
#include <iostream>
using namespace std;
// 屏蔽成员
class Area{
public:
Area(int width, int height) {
// 用this指针可以避免同名屏蔽
this->width = width;
this->height = height;
}
// 函数作用域内的参数将同名成员变量屏蔽
int getArea(int width, int height) { return width * height; } // `getArea`函数的参数`width`和`height`与类的成员变量`width` and `height`同名. 在函数内部, 参数的作用域优先级高于成员变量, 因此直接使用`width`和`height`时, 访问的是函数参数, 而非类的成员变量(即参数 "屏蔽" 了同名的成员变量).
private:
int width;
int height;
};
int main() {
Area area(3, 4);
cout << "长方形面积为:" << area.getArea(5, 6) << endl;
return 0;
}
-------------------------------------------------------------------
输出:
长方形面积为:30
1.6 静态成员
前面提到,函数的静态变量,生命周期是贯穿整个程序的(与函数内的局部变量不同),作用域仅限于函数内部(与全局变量不同)。
1.6.1 静态成员变量
类的静态成员变量,生命周期和作用域的范围都是类(而不是具体的实例),也就是在类的所有实例对象中都可见。
class Product{
public:
Product(float price) : price(price) {}
float getPrice() {
return discountRate * price;
}
private:
float price;
static float discountRate; // 静态成员变量,类内仅声明(加static)
};
// 类外初始化:必须指定类名限定,且只初始化一次
float Product::discountRate = 0.85;
int main() {
Product camera(2299.99);
cout << "相机的最终价格为:" << camera.getPrice() << endl;
Product tv(1199.99);
cout << "电视机的最终价格为:" << tv.getPrice() << endl;
return 0;
}
-----------------------------------------------------------
输出:
相机的最终价格为:1954.99
电视机的最终价格为:1019.99
静态成员变量归属于类,而不是具体的对象,所以要先于任何类对象的创建,并且在全局作用域中进行。创建时使用 static 关键字,初始化时省略,并加一个类的限定符 Product::。它和一般成员变量的区别如下,可以根据使用场景进行选择:
| 对比维度 | 非静态成员变量 | 静态成员变量 |
|---|---|---|
| 关键字 | 无static,float discountRate = 0.85; |
有static,static float discountRate; |
| 内存存储 | 每个Product对象(如camera、tv)单独存储一份 (内存中存在 2 份 0.85) |
整个Product类只存储一份,所有对象共享 (内存中仅 1 份 0.85) |
| 数据归属 | 属于「单个对象」 | 属于「类本身」,而非某个对象 |
| 访问方式 | 必须通过对象(如camera.discountRate,还需改访问权限) |
可通过类名直接访问(Product::discountRate),无需创建对象 |
| 修改影响 | 修改一个对象的discountRate,不影响其他对象 |
修改Product::discountRate,所有对象立即共享新值 |
1.6.2 静态成员函数
如果一个成员函数设计为让实例调用显得不合理,就可以尝试设计成静态成员函数,类似地,它也只能通过类名调用,
#include <iostream>
#include <string>
using namespace std;
class Student{
public:
Student(int id, string name) : id(id), name(name) {}
static int generateId() { // 静态成员函数
// 静态成员函数只能访问静态成员变量
return globalID++;
}
string getName() {
return name;
}
int getId() {
return id;
}
private:
string name;
int id;
static int globalID;
};
int Student::globalID = 1;
int main() {
Student stu1generateId(), "小宝";
cout << stu1.getName() << "的学号为:" << stu1.getId() << endl;
Student stu2generateId(), "大宝";
cout << stu2.getName() << "的学号为:" << stu2.getId() << endl;
return 0;
}
-----------------------------------------------------------
输出:
小宝的学号为:1
大宝的学号为:2
注意,static 成员函数只能访问其他的 static 成员(变量和函数),一般的成员是不能访问的(包括 this 指针)
1.6.3 常量静态成员
如上述,类的成员变量需要在构造函数中初始化,而静态成员变量则需要在类中声明,在全局域中初始化。
但是,也可以使用 const static 关键字来声明常量静态成员,它可以直接在类的定义体中初始化:
#include <iostream>
#include <string>
using namespace std;
class Student{
public:
Student(string nm, int sc) : name(nm) {
if ( sc > fullScore ) {
cout << "分数超出满分,自动调节为100!" << endl;
score = fullScore;
} else {
score = sc;
}
}
string getName() { return name; }
int getScore() { return score; }
private:
string name;
int score;
const static int fullScore = 100; // 可以在类的内部初始化常量静态成员变量
};
int main() {
Student stu1("小宝", 97);
cout << stu1.getName() << "的成绩为:" << stu1.getScore() << endl;
Student stu2("大宝", 105);
cout << stu2.getName() << "的成绩为:" << stu2.getScore() << endl;
return 0;
}
-------------------------------------------------------------
输出:
小宝的成绩为:97
分数超出满分,自动调节为100!
大宝的成绩为:100
1.7 Inheritance (继承)
一般称派生类(Derived Class)继承于基类(Dase Class)。
1.7.1 例子
// 交通工具
class Vehicle{
public:
Vehicle() { numPassengers = 0; }
void move() {}
protected: //
int numPassengers; // 乘客数量
};
// 飞机
class Airplane : public Vehicle{ // 继承自交通工具
public:
Airplane() {}
};
// 有轮交通工具
class WheeledVehicle : public Vehicle{ // 继承自交通工具
public:
WheeledVehicle() {}
};
// 汽车
class Car : public WheeledVehicle{ // 继承自有轮交通工具
public:
Car() {}
};
// 自行车
class Bicycle : public WheeledVehicle{ // 继承自有轮交通工具
public:
Bicycle() {}
};
int main() {
return 0;
}
上述代码的基类 Vehicle 的定义中,出现了一个不同于 public 和 private 的关键字 protected,三者的区别如下:
1.7.2 public、protected 和 private
| 访问权限 | 可访问者 | 主要目的 |
|---|---|---|
| public (公有) | 所有人,包括类内部、子类和外部代码。 | 定义类的公共接口。这些是供类外部用户直接调用的函数或访问的变量。 |
| protected (保护) | 类本身的成员、友元以及子类。 | 为继承提供支持。允许子类访问父类的成员,但对外部隐藏。 |
| private (私有) | 仅限于类本身的成员和友元。 | 实现封装。完全隐藏类的实现细节,外部和子类都无法直接访问。 |
在继承时,如定义体 class Airplane : public Vehicle{...} 中,使用的 public 关键字表明,子类会继承父类的非私有成员(即 public 和 protected)。注意,父类的私有成员不可继承,旨在保证类的封装性。 |
在继承方面,继承级别和基类访问级别会保留最小值,即:
- 公有继承时,基类成员的访问级别被原封不动的照搬;
- 私有继承时,访问级别全部变为私有;
- 保护继承时,
Public成员变为Protectded成员,其余不变。
对于后两者,举两个报错的例子,首先是私有继承:
class Base{
public:
Base() : a(0), b(0), c(0) { }
int a;
protected:
int b;
private:
int c;
};
class Derived : private Base{ // 私有继承
public:
Derived() { d = a + b + c; } // member "Base::c" is inaccessible,它是Base的私有成员
private:
int d;
};
class Derived1 : public Derived{
public:
Derived1(){}
int getA() { return a; } // a不可访问,它是Derived的私有成员
int getB() { return b; } // b不可访问,它是Derived的私有成员
};
int main() {
Derived1 derived1;
return 0;
}
然后是保护继承:
class Base{
public:
Base() : a(0), b(0), c(0) { }
int a;
protected:
int b;
private:
int c;
};
class Derived : protected Base{ // 保护继承
public:
Derived() { d = a + b + c; } // // member "Base::c" is inaccessible,c是Base的私有成员
private:
int d;
};
class Derived1 : public Derived{
public:
Derived1(){}
int getA() { return a; } // a和b是Derived的保护成员,子类可以访问
int getB() { return b; }
};
int main() {
Derived1 derived1;
derived1.a; // a不可访问,它是Derived1的保护成员
return 0;
}
1.7.3 Is-a 和 Has-a
有两种继承关系,也就是 Is-a (“是一种”)和 Has-a(“包含一种”)。前者的逻辑关系类似于“汽车是一种交通工具”,后者则是“汽车包含车轮、车门、引擎等等”。
# 1.7.3 #include <vector>
using namespace std;
// 交通工具
class Vehicle{
public:
Vehicle() { numPassengers = 0; }
void move() {}
protected:
int numPassengers; // 乘客数量
};
// 轮子
class Wheel{
public:
Wheel() { size = 14; }
private:
int size;
};
// 引擎
class Engine{
public:
Engine() { capacity = 2000; }
private:
int capacity; // 排量
};
// 汽车
class Car : public Vehicle{ // Car is a Vehicle
public:
Car() {}
protected:
vector<Wheel> wheels; // Car has Wheels,使用vector容器存储轮子对象
Engine engine; // Car has an Engine
};
int main() {
return 0;
}
1.7.4 派生类和基类的转换
派生类的对象可以直接访问基类的成员,因为派生类包含基类,并且成员会放在靠前的位置。如果将派生类转换为它的基类,那么派生类自己的成员将会被截断:
#include <iostream>
#include <vector>
using namespace std;
// 基类
class Base{
public:
Base() { b = 0; }
protected:
int b;
};
// 派生类
class Derived : public Base{
public:
Derived() { d = 0; }
int d;
};
int main() {
vector<Base> baseVec;
Base base;
Derived derived;
cout << "derived的大小为:" << sizeof(derived) << endl;
baseVec.push_back(base);
baseVec.push_back(derived);
cout << "放入vector的derived的大小为:" << sizeof(baseVec.back()) << endl;
// baseVec.back().d 不存在,derived已被截断
return 0;
}
----------------------------------------------------------------
输出:
derived的大小为:8
放入vector的derived的大小为:4
上述例子中,derived 实例被隐式转换为 Base 类,其成员 d 被截断,导致其 size 只有一个 int 变量的大小(变量 b 的大小)。
如果使用的是派生类的指针,那么就不会被截断:
#include <iostream>
#include <vector>
using namespace std;
// 基类
class Base{
public:
Base() { b = 0; }
protected:
int b;
};
// 派生类
class Derived : public Base{
public:
Derived() { d = 0; }
int d;
};
int main() {
Derived *derived;
cout << "derived的大小为:" << sizeof(derived) << endl;
Base *base = new Derived();
cout << "base指向derived的大小为:" << sizeof(base) << endl;
return 0;
}
输出:
derived的大小为:8
base指向derived的大小为:8
类的大小
使用 sizeof(类实例) 可以发现:
- 占据栈空间的是一般成员变量,如两个
int成员占据 8 个字节; - 静态成员与全局变量一起存放在全局数据区,不占类对象的空间。
- 成员函数和其他函数一样存放在代码区。
- 类和结构体会自动填充(padding),使其大小为 4 的倍数,具体填充策略比较复杂。
- 没有成员变量的类,大小为 1,来与邻近的类做区分。
1.7.5 继承下的构造&&析构函数
定义派生类时,如果省略基类构造函数的显式调用,系统将自动调用默认构造函数。不过以下例子有助于理解这两个函数的调用顺序:
# 1.7.5 #include <iostream>
using namespace std;
// 基类
class Base{
public:
Base() : b(0) { cout << "基类的构造函数被调用!" << endl; }
~Base() { cout << "基类的析构函数被调用!" << endl; }
protected:
int b;
};
// 派生类
class Derived : public Base{
public:
Derived() { cout << "派生类的构造函数被调用!" << endl; }
~Derived() { cout << "派生类的析构函数被调用!" << endl; }
protected:
int d;
};
int main() {
Derived derived;
return 0;
}
--------------------------------------------------------
输出:
基类的构造函数被调用!
派生类的构造函数被调用!
派生类的析构函数被调用!
基类的析构函数被调用!
在初始化类实例时,如果有参数输入构造函数,最好手动调用基类的构造函数:
#include <iostream>
using namespace std;
// 基类
class Base{
public:
Base(int bb) : b(bb) { cout << "基类的构造函数被调用!" << endl; }
~Base() { cout << "基类的析构函数被调用!" << endl; }
protected:
int b;
};
// 派生类
class Derived : public Base{
public:
Derived(int bb, int dd) : Base(bb), d(dd) { cout << "派生类的构造函数被调用!" << endl; } // 基类的构造函数被调用!
~Derived() { cout << "派生类的析构函数被调用!" << endl; }
protected:
int d;
};
int main() {
Derived derived(1, 3);
return 0;
}
1.7.6 (Multiple Inheritance) 多重继承
#include <iostream>
using namespace std;
class Support { // 辅助
public:
Support() {}
void heal() { cout << "发动治疗技能!" << endl; }
void accelerate() { cout << "发动加速技能!" << endl; }
};
class Fighter { // 战士
public:
Fighter() {}
void meleeAttack() { cout << "发动近战攻击!" << endl; }
};
class Archer { // 弓箭手
public:
Archer() {}
void rangedAttack() { cout << "发动远程攻击!" << endl; }
};
// 大boss什么都会
// 多重继承的每个基类可以给定不同的访问控制符,这里都用public
class Boss : public Support, public Fighter, public Archer { // 多重继承
public:
// 构造函数需要初始化所有基类
Boss(): Support(), Fighter(), Archer() {}
void dodge() { cout << "发动闪避!" << endl; }
void block() { cout << "发动格挡!" << endl; }
};
int main() {
Boss shiro;
shiro.rangedAttack();
shiro.accelerate();
shiro.meleeAttack();
shiro.dodge();
shiro.meleeAttack();
shiro.block();
shiro.heal();
return 0;
}
1.7.7 显式构造函数
隐式转换会自动调用构造函数,比如下面的例子,将不属于 MyClass 类的数值 5 赋值给类实例 my, 刚好 MyClass 的构造函数接受一个参数,于是就将 5 赋值给了参数 n,在 int 和 MyClass 之间的隐式转换时,自动调用了构造函数:
class MyClass {
public:
MyClass(int n) : num(n) {}
int getNum() { return num; }
private:
int num;
};
int main() {
MyClass my = 5;
cout << "my中num的值为:" << my.getNum() << endl;
return 0;
}
----------------------------------------------
输出:
my中num的值为:5
如果不允许这样的构造函数调用,可以在类定义时使用关键字 explicit 关键字来声明构造函数只能显式调用:
class MyClass {
public:
explicit MyClass(int n) : num(n) {} // 显式构造函数
int getNum() { return num; }
private:
int num;
};
int main() {
MyClass my = 5; // error: no suitable constructor exists to convert from "int" to "MyClass"
cout << "my中num的值为:" << my.getNum() << endl;
return 0;
}
1.7.8 可变数据成员
在#常量成员函数中提到,不能在其函数体中修改成员。如果需要改变成员变量,可以将这样的成员变量声明成 multable 成员,这样它就是可变的:
#include <iostream>
#include <string>
using namespace std;
class Product {
public:
Product(int i, string n, int p, int w) : id(i), name(n), price(p), weight(w) { views = 0; }
void checkInfo() const { // const 成员函数
cout << "查看商品信息:" << endl;
cout << "商品号:" << id << endl;
cout << "商品名:" << name << endl;
cout << "价格:" << price << "元" << endl;
cout << "重量:" << weight << "克" << endl;
cout << "查看次数:" << ++views << endl; // 查看次数加1
cout << endl;
}
private:
int id;
string name;
int price;
int weight;
mutable int views; // mutable变量
};
int main() {
Product prod1(1, "辣条", 3, 50);
Product prod2(2, "辣条", 3, 50);
prod1.checkInfo();
prod2.checkInfo();
prod1.checkInfo();
prod1.checkInfo();
return 0;
}
----------------------------------------------------------
输出:
查看商品信息:
商品号:1
商品名:辣条
价格:3元
重量:50克
查看次数:1
查看商品信息:
商品号:2
商品名:辣条
价格:3元
重量:50克
查看次数:1
查看商品信息:
商品号:1
商品名:辣条
价格:3元
重量:50克
查看次数:2
查看商品信息:
商品号:1
商品名:辣条
价格:3元
重量:50克
查看次数:3